/*
 * Created on Jun 5, 2004
 */
package dk.aiszone.lang.autosource.generatorobjects;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import dk.aiszone.lang.autosource.generatorobjects.helpers.ImportsList;
import dk.aiszone.lang.autosource.generatorobjects.interfaces.DeclarableObject;
import dk.aiszone.lang.autosource.generatorobjects.interfaces.GeneratorObject;

/**
 * The <code>MethodDeclaration</code> class contains the header of a <code>Method</code>.<br>
 * The <code>MethodDeclartion</code> contains the following:
 * <ul>
 * <li><code>Declaration</code>
 * <li>JavaDoc
 * <li>Return type
 * <li>Name
 * <li>arguments
 * <li>Exceptions declared by the <code>Method</code>
 * </ul>
 * 
 * @author <a href="mailto:ais@mail.dk">Ais</a>
 * @since 1.0
 * @version 1.0
 */
public class MethodDeclaration extends GeneratorObject implements Comparable<MethodDeclaration>, Cloneable, DeclarableObject {

    private List<Annotation> annotations;
    private List<Argument> arguments;
    private Declaration declaration;
    private Set<Class> exceptions;

    private JavaDoc javaDoc;
    private String name;

    private ImportsList neededImports;

    private Class returnType;

    /**
     * Default constructor for the <code>MethodDeclaration</code> class.<br>
     * Using the constructor requires the user to later on set the <code>name</code>, <code>Declaration</code> and <code>returnType</code><br>
     * on the <code>MethodDeclaration</code>.
     * 
     * @see #setName(String)
     * @see #setDeclaration(Declaration)
     * @see #setReturnType(Class)
     */
    public MethodDeclaration() {
        this.arguments = new ArrayList<Argument>();
        this.exceptions = new LinkedHashSet<Class>();
        this.annotations = new ArrayList<Annotation>();

        this.neededImports = new ImportsList();
    }

    /**
     * Constructor for the <code>MethodDeclaration</code> class.
     * 
     * @param declaration
     *            The <code>Declaration</code> of the <code>Method</code>
     * @param returnType
     *            The return type
     * @param name
     *            The name of the <code>Method</code>
     */
    public MethodDeclaration(Declaration declaration, Class returnType, String name) {
        this();

        this.declaration = declaration;
        this.returnType = returnType;
        this.name = name;

        if (returnType != null) {
            addNeededImports(returnType.toImports());
        }
    }

    @Override
    public void addAnnotation(Annotation annotation) {
        if (annotation == null) {
            throw new NullPointerException();
        }
        annotations.add(annotation);
    }

    /**
     * Add an additional <code>Argument</code> to the <code>MethodDeclaration</code>
     * 
     * @param argument
     *            The <code>Argument</code> to add.
     */
    public void addArgument(Argument argument) {
        insertArgument(arguments.size(), argument);
    }

    /**
     * Adds additional <code>Argument</code> objects to the <code>MethodDeclaration</code> based on the supplied array.<br>
     * This method is an convinience method - when a <code>MethodDeclaration</code> is based on data retrieved using reflection.<br>
     * The <code>java.lang.Class</code> objects found in the array, will be added in the order found - and the names of the<br>
     * of arguments is based on the class type. Example: The first <code>long</code> is called _long, the second _long1, and so on.<br>
     * The javadoc of the <code>Argument</code> will be autogenerated, to something general, but meaningless.
     * 
     * @param argumentTypes
     *            The <code>Array</code> of <code>java.lang.Class</code> objects to convert to arguments.
     */
    public void addArguments(java.lang.Class<?>[] argumentTypes) {
        Map<String, CountHolder> counts = new HashMap<String, CountHolder>();

        for (int i = 0; i < argumentTypes.length; i++) {
            java.lang.Class<?> _class = argumentTypes[i];

            Class type = new Class(_class.getName());

            CountHolder count = counts.get(type.getClassName());
            if (count == null) {
                count = new CountHolder();
                counts.put(type.getClassName(), count);
            }
            count.count++;

            Argument argument = new Argument();
            argument.setType(type);
            if (count.count == 1) {
                argument.setName((type.isPrimitive() ? "_" : "") + type.getClassName());
            } else {
                argument.setName((type.isPrimitive() ? "_" : "") + type.getClassName() + (count.count - 1));
            }
            argument.setJavaDoc("The value of the <code>" + argument.getName() + "</code> attribute.");

            addArgument(argument);
        }
    }

    /**
     * Add an exception declaration to the <code>Method</code>
     * 
     * @param exception
     *            <code>Class</code> representing the expcetion to add.
     */
    public void addException(Class exception) {
        if (exception == null) {
            throw new NullPointerException();
        }
        exceptions.add(exception);
    }

    /**
     * Add a <code>Import</code> needed by the <code>Method</code>.<br>
     * One special <code>Imports</code> are needed added - <code>Imports</code> needed by arguments, exception or generics<br>
     * are handled internally in the <code>MethodDeclaration</code>.
     * 
     * @param _import
     *            <code>Import</code> to add.
     */
    public void addNeededImport(Import _import) {
        neededImports.addImport(_import);
    }

    public int compareTo(MethodDeclaration other) {
        int val = declaration.compareTo(other.declaration);
        if (val == 0) {
            if (name != null && other.name != null) {
                val = name.compareTo(other.name);
            }
            if (val == 0) {
                int argCount1 = getArguments().size();
                int argCount2 = other.getArguments().size();

                if (argCount1 < argCount2) {
                    return -1;
                } else if (argCount1 > argCount2) {
                    return 1;
                }
                return 0;
            }
            return val;
        }
        return val;
    }

    @Override
    public List<Annotation> getAnnotations() {
        return annotations;
    }

    /**
     * Retrieve a <code>List</code> of the <code>Arguments</code> declared by the <code>Method</code>
     * 
     * @return <code>List</code> of <code>Arguments</code> (<code>List&lt;Argument&gt:</code>).<br>
     *         If no arguments are declared by the <code>Method</code> an empty <code>List</code> is returned.
     */
    public List<Argument> getArguments() {
        List<Argument> result = new ArrayList<Argument>();
        for (Iterator<Argument> iterator = arguments.iterator(); iterator.hasNext();) {
            Argument argument = iterator.next();
            if (argument != null) {
                result.add(argument);
            }
        }

        return result;
    }

    /**
     * Retrieve the <code>Declaration</code> of the <code>Method</code>.
     * 
     * @return The method declaration - or <code>null</code> if unset.
     */
    public Declaration getDeclaration() {
        return declaration;
    }

    /**
     * Retrieve a <code>Set</code> of the <code>Exceptions</code> declared by the <code>Method</code>.
     * 
     * @return <code>Set</code> of expection <code>Classes</code> (<code>Set&lt;Class&gt;</code>).<br>
     *         If no arguments are declared by the <code>Method</code> an empty <code>Set</code> is returned.
     */
    public Set<Class> getExceptions() {
        return exceptions;
    }

    /**
     * Retrieve the <code>JavaDoc</code> of the <code>MethodDeclaration</code>
     * 
     * @return The declared <code>JavaDoc</code> object.
     */
    public JavaDoc getJavaDoc() {
        if (javaDoc == null) {
            setJavaDoc(new JavaDoc());
        }

        return javaDoc;
    }

    /**
     * Retrieve the name of the <code>Method</code>
     * 
     * @return The method name - or <code>null</code> if unset.
     */
    public String getName() {
        return name;
    }

    /**
     * Retrieve a <code>ImportList</code> containing all the <code>Imports</code> needed by the <code>MethodDeclaration</code>.
     * 
     * @return The needed <code>Imports</code>.
     */
    public ImportsList getNeededImports() {
        for (Iterator<Argument> iterator = getArguments().iterator(); iterator.hasNext();) {
            Argument argument = iterator.next();
            addNeededImports(argument.getType().toImports());
        }

        for (Iterator<Annotation> iterator = annotations.iterator(); iterator.hasNext();) {
            Annotation annotation = iterator.next();
            neededImports.addAll(annotation.getNeededImports());
        }

        for (Iterator<Class> iterator = exceptions.iterator(); iterator.hasNext();) {
            Class exception = iterator.next();
            addNeededImports(exception.toImports());
        }

        return neededImports;
    }

    /**
     * Retrieve the return type of the <code>MethodDeclaration</code>
     * 
     * @return <code>Class</code> representing the return type of the <code>Method</code>.
     */
    public Class getReturnType() {
        return returnType;
    }

    /**
     * Inserts the supplied <code>Argument</code> at the specified index.<br>
     * If the specified index is larger then the current number of arguments,<br>
     * a number of <code>null</code> arguments will be inserted to expand the<br>
     * arguments <code>List</code>.<br>
     * However calling <code>getArgument()</code> will return a <code>List</code><br>
     * where these <code>null</code> arguments will have been removed.<br>
     * If an <code>Argument</code> already exists at the specified index, then<br>
     * the existing will be pushed to index <code>index + 1</code>.
     * 
     * @param index
     *            The index to insert the <code>Argument</code> at.
     * @param argument
     *            The <code>Argument</code> to insert.
     */
    public void insertArgument(int index, Argument argument) {
        if (index >= arguments.size()) {
            for (int i = arguments.size() - 1; i < index; i++) {
                arguments.add(null);
            }
        }

        if (arguments.get(index) == null) {
            arguments.add(index, argument);
            arguments.remove(index + 1);
        } else {
            arguments.add(index, argument);
        }
    }

    /**
     * Remove the specified <code>Argument</code> from the <code>List</code> of <code>Arguments</code>.
     * 
     * @param argument
     *            The <code>Argument</code> to remove.
     * @return <code>true</code> if the <code>Argument</code> was removed - otherwise <code>false</code>.
     */
    public boolean removeArgument(Argument argument) {
        return arguments.remove(argument);
    }

    /**
     * Removes the <code>Argument</code> at the specified index.
     * 
     * @param index
     *            The index of the <code>Argument</code> to be removed.
     * @throws IndexOutOfBoundsException
     *             Thrown if the specified index is larger then the number of <code>Arguments</code>.
     */
    public void removeArgument(int index) throws IndexOutOfBoundsException {
        arguments.remove(index);
    }

    /**
     * Set the <code>Declaration</code> of the <code>Method</code>.
     * 
     * @param declaration
     *            The method declaration - or <code>null</code> to unset.
     */
    public void setDeclaration(Declaration declaration) {
        this.declaration = declaration;
    }

    /**
     * Set the <code>JavaDoc</code> of the <code>Method</code>.
     * 
     * @param javaDoc
     *            The documentation of the <code>Method</code>
     */
    public void setJavaDoc(JavaDoc javaDoc) {
        this.javaDoc = javaDoc;
        if (javaDoc != null) {
            javaDoc.setParentMethod(this);
        }
    }

    /**
     * Set the name of the <code>Method</code>
     * 
     * @param name
     *            The method name - or <code>null</code> to unset.
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Set the return type of the <code>MethodDeclaration</code>
     * 
     * @param returnType
     *            The <code>Class</code> representing the return type of the <code>Method</code>.
     */
    public void setReturnType(Class returnType) {
        this.returnType = returnType;
        addNeededImports(returnType.toImports());
    }

    /**
     * Add the supplied <code>Imports</code> to the <code>Imports</code> needed by the <code>AbstractBody</code>.
     * 
     * @param _imports
     *            <code>Set</code> of needed <code>Imports</code> (<code>Set&lt;Import&gt;</code>)
     */
    protected void addNeededImports(Set<Import> _imports) {
        neededImports.addImports(_imports);
    }

    @Override
    protected Object clone() {
        MethodDeclaration clone = new MethodDeclaration();

        cloneAttributes(clone);

        return clone;
    }

    /**
     * Deep clones every attribute on the <code>MethodDeclaration</code> objects with the exception of the parent attribute.<br>
     * This attribute is not cloned to the new object.
     * 
     * @param clone
     *            <code>MethodDeclaration</code> object to be popultated with the cloned values of this object
     */
    protected void cloneAttributes(MethodDeclaration clone) {
        for (Iterator<Argument> iter = getArguments().iterator(); iter.hasNext();) {
            clone.addArgument((Argument)iter.next().clone());
        }

        clone.declaration = (Declaration)declaration.clone();

        for (Iterator<Class> iter = exceptions.iterator(); iter.hasNext();) {
            clone.addException((Class)iter.next().clone());
        }

        clone.javaDoc = javaDoc;
        clone.name = name;
        clone.returnType = (Class)returnType.clone();
        clone.neededImports = (ImportsList)neededImports.clone();
    }

    private class CountHolder {

        /* pp */int count;
    }
}